<?php

namespace App\Services;

use App\Models\Prize;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Log;

/**
 * 抽奖服务类
 * 
 * 使用Redis优化的抽奖系统，主要特点：
 * 1. 使用Redis缓存奖品列表，减少数据库查询
 * 2. 使用Redis原子操作管理库存，保证并发安全
 * 3. 使用Redis Set存储中奖用户，支持快速查询
 * 4. 异步更新数据库，提高系统响应速度
 */
class LotteryService
{
    /**
     * Redis缓存键前缀定义
     * 
     * CACHE_KEY_PRIZES: 奖品列表缓存键
     * CACHE_KEY_PRIZE_STOCK: 奖品库存缓存键前缀，实际使用时后面加上奖品ID
     * CACHE_KEY_WINNERS: 每日已中奖用户集合键，使用 Redis Set 存储
     */
    const CACHE_KEY_PRIZES = 'lottery:prizes';
    const CACHE_KEY_PRIZE_STOCK = 'lottery:stock:';
    const CACHE_KEY_WINNERS = 'lottery:winners:daily';
    
    /**
     * 缓存过期时间（秒）
     * 
     * CACHE_TTL: 奖品列表缓存时间，默认5分钟
     * WINNERS_TTL: 中奖用户集合过期时间，每天凌晨过期
     */
    const CACHE_TTL = 300; // 5分钟
    const WINNERS_TTL = 86400; // 24小时
    
    /**
     * 执行抽奖
     * 
     * 抽奖流程：
     * 1. 检查用户是否已中奖（使用SISMEMBER命令）
     * 2. 获取可用奖品列表（从缓存）
     * 3. 根据概率选择奖品
     * 4. 扣减库存（原子操作）
     * 5. 记录中奖信息（使用SADD命令）
     *
     * @param int $userId 用户ID
     * @return array|null 中奖信息数组或空（未中奖）
     */
    public function draw(int $userId): ?array
    {
        Log::info('开始抽奖', [
            'user_id' => $userId,
            'time' => date('Y-m-d H:i:s')
        ]);

        // 检查用户是否已中奖
        if ($this->checkUserWinToday($userId)) {
            Log::info('用户今日已中奖，退出抽奖', [
                'user_id' => $userId
            ]);
            return null;
        }

        // 获取所有可用奖品（从缓存）
        $prizes = $this->getAvailablePrizes();
        
        if (empty($prizes)) {
            Log::warning('没有可用奖品');
            return null;
        }

        // 计算总概率
        $totalProbability = collect($prizes)->sum('probability');
        
        // 生成随机数（1-10000之间）并转换为0-1之间的小数
        $random = mt_rand(1, 10000) / 10000;
        $currentProbability = 0;

        Log::debug('概率计算', [
            'random_number' => $random,
            'total_probability' => $totalProbability,
        ]);

        // 根据概率选择奖品（概率累加法）
        foreach ($prizes as $prize) {
            $currentProbability += $prize['probability'] / $totalProbability;
            
            Log::debug('奖品概率检查', [
                'random' => $random,
                'prize_id' => $prize['id'],
                'prize_name' => $prize['name'],
                'current_probability' => $currentProbability,
            ]);
            
            if ($random <= $currentProbability) {
                // 尝试扣减库存（原子操作）
                if ($this->decrementStock($prize['id'], $userId)) {
                    // 记录用户中奖
                    $this->recordUserWin($userId);
                    
                    $result = [
                        'prize_id' => $prize['id'],
                        'name' => $prize['name'],
                        'user_id' => $userId
                    ];

                    Log::info('抽奖成功', [
                        'user_id' => $userId,
                        'prize_id' => $prize['id'],
                        'prize_name' => $prize['name']
                    ]);

                    return $result;
                } else {
                    Log::warning('奖品库存扣减失败', [
                        'prize_id' => $prize['id'],
                        'prize_name' => $prize['name']
                    ]);
                }
            }
        }

        Log::info('未中奖', ['user_id' => $userId]);
        return null;
    }

    /**
     * 获取可用奖品列表（带缓存）
     * 
     * 使用Cache::remember实现缓存：
     * 1. 优先从缓存获取
     * 2. 缓存不存在时从数据库获取并缓存
     * 3. 同时初始化Redis库存
     *
     * @return array 可用奖品列表
     */
    protected function getAvailablePrizes(): array
    {
        Log::debug('获取奖品列表');
        return Cache::remember(self::CACHE_KEY_PRIZES, self::CACHE_TTL, function () {
            // 先获取奖品列表
            $prizes = Prize::where('status', 1)
                ->where('stock', '>', 0)
                ->get();

            // 初始化 Redis 库存（这时还是对象）
            foreach ($prizes as $prize) {
                $this->initializeStock($prize->id, $prize->stock);
            }

            // 转换为数组并记录日志
            $prizesArray = $prizes->toArray();
            
            Log::info('刷新奖品缓存', [
                'count' => count($prizesArray),
                'prizes' => array_map(function($prize) {
                    return [
                        'id' => $prize['id'],
                        'name' => $prize['name'],
                        'stock' => $prize['stock']
                    ];
                }, $prizesArray)
            ]);

            return $prizesArray;
        });
    }

    /**
     * 初始化奖品库存到Redis
     * 
     * 使用Redis::exists和Redis::set实现：
     * 1. 检查库存键是否存在
     * 2. 不存在时初始化库存数量
     *
     * @param int $prizeId 奖品ID
     * @param int $stock 库存数量
     * @return void
     */
    protected function initializeStock(int $prizeId, int $stock): void
    {
        $key = self::CACHE_KEY_PRIZE_STOCK . $prizeId;
        
        // 使用 SETNX 原子操作，只在键不存在时设置
        $result = Redis::setnx($key, $stock);
        
        if ($result) {
            Log::info('初始化奖品库存', [
                'prize_id' => $prizeId,
                'stock' => $stock,
                'key' => $key
            ]);
        } else {
            // 如果键已存在，记录当前库存
            $currentStock = Redis::get($key);
            Log::debug('奖品库存已存在', [
                'prize_id' => $prizeId,
                'current_stock' => $currentStock,
                'key' => $key
            ]);
        }
    }

    /**
     * 扣减库存（原子操作）
     * 
     * 使用Redis Lua脚本实现原子操作：
     * 1. 检查库存是否大于0
     * 2. 是则扣减库存并返回true
     * 3. 否则返回false
     * 4. 扣减成功后异步更新数据库
     *
     * @param int $prizeId 奖品ID
     * @param int $userId 用户ID
     * @return bool 是否扣减成功
     */
    protected function decrementStock(int $prizeId, int $userId): bool
    {
        $key = self::CACHE_KEY_PRIZE_STOCK . $prizeId;

        // 使用Redis原子操作扣减库存
        $result = Redis::eval(<<<LUA
            local current = redis.call('get', KEYS[1])
            if current and tonumber(current) > 0 then
                redis.call('decr', KEYS[1])
                return true
            end
            return false
        LUA, 1, $key);

        if ($result) {
            // 使用异步更新数据库和记录中奖信息
            dispatch(new \App\Jobs\LotteryDrawTask(
                $prizeId,
                $userId,
                date('Y-m-d H:i:s')
            ));
            
            $newStock = Redis::get($key);
            
            Log::info('库存扣减成功', [
                'prize_id' => $prizeId,
                'user_id' => $userId,
                'current_stock' => $newStock,
                'key' => $key
            ]);
            
            return true;
        }

        Log::warning('库存不足', [
            'prize_id' => $prizeId,
            'user_id' => $userId,
            'current_stock' => Redis::get($key),
            'key' => $key
        ]);

        return false;
    }

    /**
     * 异步更新数据库库存
     * 
     * 已改为使用队列异步处理，此方法废弃
     *
     * @deprecated
     * @param int $prizeId 奖品ID
     * @return void
     */
    protected function updateDatabaseStock(int $prizeId): void
    {
    }

    /**
     * 检查用户今日是否已中奖
     * 
     * 使用Redis Set的SISMEMBER命令检查用户是否在中奖集合中：
     * 1. 键：lottery:winners:daily
     * 2. O(1)时间复杂度
     * 3. 自动过期清理
     *
     * @param int $userId 用户ID
     * @return bool 是否已中奖
     */
    protected function checkUserWinToday(int $userId): bool
    {
        $this->initWinnersSet();

        $isWinner = Redis::sismember(self::CACHE_KEY_WINNERS, $userId);
        
        Log::debug('检查用户是否已中奖', [
            'user_id' => $userId,
            'is_winner' => $isWinner
        ]);

        return $isWinner;
    }

    /**
     * 记录用户中奖
     * 
     * 使用Redis Set的SADD命令记录中奖用户：
     * 1. 添加用户到中奖集合
     * 2. O(1)时间复杂度
     * 3. 自动去重
     *
     * @param int $userId 用户ID
     * @return void
     */
    protected function recordUserWin(int $userId): void
    {
        $this->initWinnersSet();

        Redis::sadd(self::CACHE_KEY_WINNERS, $userId);
        
        Log::info('记录用户中奖', [
            'user_id' => $userId,
            'time' => date('Y-m-d H:i:s')
        ]);
    }

    /**
     * 初始化中奖用户集合
     * 
     * 确保中奖用户集合存在并设置过期时间：
     * 1. 使用Redis的EXISTS命令检查集合是否存在
     * 2. 不存在时创建并设置过期时间
     * 3. 过期时间设置到第二天凌晨
     *
     * @return void
     */
    protected function initWinnersSet(): void
    {
        $key = self::CACHE_KEY_WINNERS;

        if (!Redis::exists($key)) {
            $ttl = strtotime('tomorrow midnight') - time();
            
            Redis::multi()
                ->sadd($key, [])
                ->expire($key, $ttl)
                ->exec();
        }
    }
} 